package ca.compsci.opent.compiler.semantics;

import ca.compsci.opent.compiler.analysis.AnalysisAdapter;
import ca.compsci.opent.compiler.node.*;

import java.util.HashSet;
import java.util.Set;

public class BinOprTypeValidator extends AnalysisAdapter  {	
	private Set<Type> permitted_types = new HashSet<Type>(9);
	
	private Type left_type, right_type;
	
	private POpr opr = null;
	
	public BinOprTypeValidator(Type left, Type right) {
		this.left_type = left;
		this.right_type = right;
	}
	
	// Note that validate() must be called at some point
	// before getResultType() is called, to ensure argument
	// types confirm to permitted_types
	public void validate() throws LeftTypeMismatchException, 
			RightTypeMismatchException, TypeInequalityException {
		
		// if the either type is marked ERRONEOUS, then skip validation
		if (left_type.equals(Type.ERRONEOUS) || right_type.equals(Type.ERRONEOUS))
			return;
		
		if (booleanRequiresMatchingArgTypes() && !left_type.equals(right_type))
			throw new TypeInequalityException();
		
		// checks to see if the argument type is one of the allowed types for the operator
		if (!permitted_types.contains(left_type))
			throw new LeftTypeMismatchException(generateExpectedTypesMsg());
		
		if (!permitted_types.contains(right_type))
			 throw new RightTypeMismatchException(generateExpectedTypesMsg());
		
		// Special case for the + operator (the only other operator can have boolean operands)
		if (opr instanceof AAddOpr && left_type.equals(Type.Boolean) && !right_type.equals(Type.String))
			throw new RightTypeMismatchException(Type.String.toString());
		
		if (opr instanceof AAddOpr && right_type.equals(Type.Boolean) && !left_type.equals(Type.String))
			throw new LeftTypeMismatchException(Type.String.toString());
	}
	
	// Returns the type that results after using the operator
	// This accounts for type promotion
	public Type getResultantType() {
		if (booleanRequiresMatchingArgTypes())
			return Type.Boolean;
		
		return getPromotedType();
	}
	
	public Type getPromotedType() {
		return getPromotedType(left_type, right_type);
	}
	
	public static Type getPromotedType(Type left_type, Type right_type) {
		// Type priority/precedence is defined in the Type enum
		for (Type t : Type.values())
			if (t.pattern() != null && (left_type.equals(t) || right_type.equals(t)))
				return t;
		
		return null;
	}
	
	public boolean hasPromoted() {
		return !(left_type.equals(Type.Reference) 
				|| right_type.equals(Type.Reference) 
				|| left_type.equals(right_type));
	}

	@Override
	public void caseAAndOpr(AAndOpr node) {
		permitted_types.clear();
		permitBoolean();
		opr = node;
	}

	@Override
	public void caseAOrOpr(AOrOpr node) {
		permitted_types.clear();
		permitBoolean();
		opr = node;
	}

	@Override
	public void caseAEqOpr(AEqOpr node) {
		permitted_types.clear();
		permitString();
		permitBoolean();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseANoteqOpr(ANoteqOpr node) {
		permitted_types.clear();
		permitString();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseAGteqOpr(AGteqOpr node) {
		permitted_types.clear();
		permitString();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseAGtOpr(AGtOpr node) {
		permitted_types.clear();
		permitString();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseALteqOpr(ALteqOpr node) {
		permitted_types.clear();
		permitString();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseALtOpr(ALtOpr node) {
		permitted_types.clear();
		permitString();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseAAddOpr(AAddOpr node) {
		permitted_types.clear();
		permitString();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseASubOpr(ASubOpr node) {
		permitted_types.clear();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseADivOpr(ADivOpr node) {
		permitted_types.clear();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseAModOpr(AModOpr node) {
		permitted_types.clear();
		permitIntegers();
		opr = node;
	}

	@Override
	public void caseAMulOpr(AMulOpr node) {
		permitted_types.clear();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseAPowerOpr(APowerOpr node) {
		permitted_types.clear();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseAShlOpr(AShlOpr node) {
		permitted_types.clear();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseAShrOpr(AShrOpr node) {
		permitted_types.clear();
		permitAllNumerics();
		opr = node;
	}

	@Override
	public void caseAXorOpr(AXorOpr node) {
		permitted_types.clear();
		permitAllNumerics();
		opr = node;
	}
	
	private void permitAllPrimitives() {
		permitAllNumerics();
		permitString();
		permitBoolean();
	}
	
	private void permitAllNumerics() {
		permitIntegers();
		permitFloatingPoints();
	}
	
	private void permitIntegers() {
		permitted_types.add(Type.Integer64);
		permitted_types.add(Type.Integer32);
		permitted_types.add(Type.Integer16);
		permitted_types.add(Type.Integer8);
	}
	
	private void permitFloatingPoints() {
		permitted_types.add(Type.Real64);
		permitted_types.add(Type.Real32);
	}
	
	private void permitBoolean() {
		permitted_types.add(Type.Boolean);
	}
	
	private void permitString() {
		permitted_types.add(Type.String);
	}
	
	private void permitReference() {
		permitted_types.add(Type.Reference);
	}
	
	// returns true if the operator is of the kind that requires
	// arguments with matching type and has a result type of Type.Boolean
	private boolean booleanRequiresMatchingArgTypes() {
		return opr instanceof AEqOpr
		    || opr instanceof ANoteqOpr
	        || opr instanceof AGtOpr
	        || opr instanceof AGteqOpr
	        || opr instanceof ALtOpr
	        || opr instanceof ALteqOpr;
	}
	
	private String generateExpectedTypesMsg() {
		StringBuilder msg = new StringBuilder();
		
		Type[] types = new Type[0];
		types = permitted_types.toArray(types);
		int i;
		
		for (i = 0; i < types.length - 2; i++)
			msg.append(types[i])
			   .append(',')
			   .append(' ');
		
		msg.append(types[i]);
		
		if (types.length > 1)
		   msg.append(" or ").append(types[i + 1]);
		
		return msg.toString();
	}
}
